Dla praktykw: C++
------------------


Wyhoduj sobie... komputer
-------------------------

Jedn z bardzo interesujcych prb podejcia do problemu
optymalizacji globalnej s algorytmy genetyczne. Znajduj
one wiele zastosowa, zwaszcza w przypadkach
nierozwizywalnych metod "brute force", gdy przegldnicie
caej przestrzeni rozwiza jest niewykonalne ze wzgldu
na zoono obliczeniow. Realne przykady zastosowa to
problemy kombinatoryczne (klasycznym takim zadaniem jest
problem komiwojaera), opracowywanie nowych ksztatw
opatek silnikw odrzutowych, symulowanie ycia (artifical
life), modelowanie czsteczek zwizkw chemicznych.

Istot algorytmw genetycznych jest przedstawienie
niewielkiego podzbioru rozwiza problemu jako populacji
osobnikw. Kady osobnik w populacji reprezentowany jest
przez chromosom - w najprostszym przypadku bdcy cigiem
binarnym. Na podstawie tego cigu mona obliczy parametr
charakteryzujcy osobnika - tzw. funkcj przystosowania,
determinujc prawdopodobiestwo wystpienia osobnika w
kolejnej generacji. W ten sposb realizuje si mechanizm
zwany w przyrodzie doborem naturalnym, a dajcy sprowadzi
si do maksymalizacji funkcji przystosowania. Zadaniem
programisty korzystajcego z algorytmw genetycznych jest
obliczenie funkcji przystosowania na podstawie zawartoci
chromosomu. Na przykad w zadaniu polegajcym na
minimalizacji funkcji wielu zmiennych chromosom moe
stanowi nastpujce kolejno po sobie binarne reprezentacje
argumentw funkcji, a warto funkcji przystosowania
odpowiada zanegowanej wartoci funkcji minimalizowanej. W
ten sposb w kolejnych generacjach preferowane bd coraz
nisze wartoci badanej funkcji.

Jeeli populacja osobnikw powinna spenia pewne warunki
brzegowe (na przykad dziedzina funkcji powinna nalee do
okrelonego przedziau), mona wykorzysta funkcj kary.
Jej implementacja polega na silnym zmniejszeniu funkcji
przystosowania w przypadku osobnikw nie speniajcych
warunkw brzegowych.

Chromosomy podlegaj operacjom genetycznym, modyfikujcym
ich zawarto. Kada operacja dokonuje si z zaoonym z
gry prawdopodobiestwem. Mutacja polega na zastpieniu
pojedynczego bitu (genu) w chromosomie bitem przeciwnym.
Inwersja - na zamianie kolejnoci bitw w losowo wybranym
podcigu chromosomu. Crossing-over polega na krzyowej
wymianie fragmentw dwch chromosomw. Oczywicie mona
wyobrazi sobie znacznie wicej operacji genetycznych (np.
rotacj), ale korzyci z ich zastosowania zale od typu
rozwizywanego problemu.

<RYSUNEK 1>

Podstawowe operacje genetyczne:

Mutacja:
  Ŀ              Ŀ
  01101110------------->01111110
                
         ^                              ^
   zmutowany gen


Inwersja:
  Ŀ              Ŀ
  01110110------------->01011110
                
                                 
   obszar inwersji


Crossing-over:
  Ŀ              Ŀ
  00011110----\  /----->11011110
       \/       
  Ŀ     /\       Ŀ
  11010111----/  \----->00010111
                
         ^                              ^
     pozycja CO

<KONIEC RYSUNKU 1>

Po wykonaniu operacji genetycznych nastpuje etap selekcji,
polegajcy na wybraniu najbardziej wartociowych osobnikw
do nastpnego pokolenia. Prawdopodobiestwo wyboru osobnika
jest zwykle zwizane ze wzgldn wartoci funkcji
przystosowania (tzw. regua ruletki, w ktrej
prawdopodobiestwo to jest wprost proporcjonalne do wartoci
funkcji przystosowania). Stosuje si tutaj rozmaite
modyfikacje, dba o rnorodno populacji (na przykad
zapewnia przeywalno rwnie niewielkiej grupie osobnikw
najgorzej przystosowanych), starajc si jednoczenie nie
straci najlepszych dotychczas otrzymanych osobnikw
(realizuje si to przez utworzenie tzw. grupy elitarnej,
nie podlegajcej modyfikacjom w kolejnym pokoleniu). Po
dokonaniu selekcji ponownie przeprowadza si operacje
genetyczne i powtarza si ten cykl a do chwili znalezienia
zadowalajcego rozwizania (lub utraty cierpliwoci).

Przedstawiajc ide algorytmw genetycznych chciaem
pokaza ich prost, a jednoczenie niebanaln aplikacj.
Zaprezentowany poniej program (w C++) stanowi prb
implementacji algorytmu genetycznego do rozwizania
zmodyfikowanego klasycznego zadania z dziedziny
optymalizacji, tzw. problemu plecakowego (knapsack
problem). W oryginalnym ujciu problem ten, znany chyba
kademu turycie, polega na takim zapakowaniu plecaka, aby
zabra do niego jak najwicej potrzebnych przedmiotw,
redukujc jednoczenie do minimum ich ciar.

Waciwym zadaniem programu jest znalezienie optymalnej
konfiguracji kupowanego (lub skadanego samodzielnie)
systemu komputerowego - tak, aby za jak najnisz sum
naby sprzt o najlepszych parametrach. Kada konfiguracja
reprezentowana jest przez pojedynczy chromosom. Wiksze
prawdopodobiestwo przeycia maj osobniki lepiej
przystosowane, czyli w naszym przypadku - konfiguracje o
wyszym stosunku moliwoci do ceny. W ten sposb "hodujemy"
populacj komputerw o coraz lepszych parametrach (std
nieco zaskakujcy tytu artykuu). Oczywicie cay program
naley traktowa z przymrueniem oka, chodzio mi jedynie
o pokazanie konkretnego zastosowania algorytmu. Przed
uruchomieniem programu naley utworzy odpowiedni plik
"OFERTA.TXT", zawierajcy dostpne podzespoy, ich ceny,
oraz - co najwaniejsze - subiektywnie okrelon warto
(przydatno) kadej czci. Uzupenienie tego pliku
pozostawiam Czytelnikom (ceny na pewno nie bd aktualne).
Przy niewielkiej liczbie podzespow optymalne rozwizanie
zostaje znalezione prawie natychmiast, warto wic rozbudowa
list czci.

Najwaniejsz cz programu stanowi klasa Genetic.
Umoliwia ona stworzenie populacji binarnych chromosomw
wraz z grup elitarn. Klasa wykorzystuje najprostsze
operacje genetyczne - mutacj oraz crossing-over. Aby
wykorzysta algorytmy genetyczne we wasnym programie
naley utworzy klas potomn klasy Genetic oraz
zaimplementowa w niej funkcj GetFitness. Funkcja ta
powinna zadba o obliczenie przystosowania osobnika oraz
zbada, czy spenia on zaoone warunki brzegowe. W celu
konwersji chromosomu na warto rzeczywist mona skorzysta
z funkcji GetBinValue. Nastpnie pozostaje tylko stworzenie
obiektu klasy potomnej oraz iteracyjne wywoywanie funkcji
Epoch, a do chwili znalezienia zadowalajcego rozwizania
problemu.

Wicej informacji na temat algorytmw genetycznych mona
znale za porednictwem sieci Internet m.in. w archiwum
SimTel i w ftp.funet.fi oraz w usenetowej licie
comp.ai.genetic.


Literatura:

David F. Goldberg, "Algorytmy genetyczne i ich zastosowania",
WNT 1995


<WYDRUK 1 - PLIK GENETIC.H>

//  GENETIC.H
//  Implementacja prostego algorytmu genetycznego
//  Piotr Rotkiewicz, piro@chem.uw.edu.pl, 1995
//  Borland C++ 3.1

#include <stdlib.h>
#include <values.h>

class Genetic { // klasa podstawowa

private:
    char **Population, **NewPopulation, **Elite;
    float *FitnessTable;
    int PopulationSize, ChromosomeSize, EliteSize;
    float MutationProb, COProb, SelectionFactor;
    float RND(void)
  { return (float)random(MAXINT)/(float)MAXINT; }
    void RandomizeChromosome(char *Chromosome);

public:
    Genetic(int, int, int); // konstruktor
    ~Genetic(void); // destruktor

    void Mutation(void); // operacja mutacji
    void CrossingOver(void); // operacja crossing over
    void Selection(void); // operacja selekcji
    void Epoch(void); // pokolenie

    float GetBinValue(char*, int, int);
    // konwertuje chromosom do wartoci rzeczywistej

    virtual float GetFitness(char*, int)
      { return 0; } // funkcja przystosowania

    virtual void ShowChromosome(char*, int)
      { } // pokazuje najlepszy chromosom
}; // Genetic

<KONIEC WYDRUKU 1>



<WYDRUK 2 - PLIK GENETIC.CPP>

// GENETIC.CPP
// implementacja klasy Genetic

#include <iostream.h>
#include <math.h>

#include "genetic.h"

Genetic::Genetic(int PopulationSize, int EliteSize,
     int ChromosomeSize)
// konstruktor
{
  Genetic::PopulationSize = PopulationSize;
  Genetic::ChromosomeSize = ChromosomeSize;
  Genetic::EliteSize = EliteSize;

    Population = new char* [PopulationSize];
    if (Population)
      for (int i=0; i<PopulationSize; i++) {
        Population[i] = new char [ChromosomeSize];
        RandomizeChromosome(Population[i]);
      }

    NewPopulation = new char* [PopulationSize];
    if (NewPopulation)
      for (int i=0; i<PopulationSize; i++)
        NewPopulation[i] = new char [ChromosomeSize];

    Elite = new char* [EliteSize];
    if (Elite)
      for (int i=0; i<EliteSize; i++) {
        Elite[i] = new char [ChromosomeSize];
        RandomizeChromosome(Elite[i]);
      }

    FitnessTable = new float [PopulationSize];

    randomize();

    // domylne wartoci parametrw symulacji
    MutationProb = 0.005;
    COProb = 0.40;
    SelectionFactor = 0.90;
} // Genetic

Genetic::~Genetic(void)
// destruktor
{
    for (int i=0; i<PopulationSize; i++)
      delete[] Population[i];
    delete[] Population;

    for (i=0; i<PopulationSize; i++)
      delete[] NewPopulation[i];
    delete[] NewPopulation;

    for (i=0; i<EliteSize; i++)
      delete[] Elite[i];
    delete[] Elite;

    delete [] FitnessTable;
} // ~Genetic

void Genetic::Mutation(void)
// operacja mutacji
{
    for (int i=0; i<PopulationSize; i++)
      for (int j=0; j<ChromosomeSize; j++)
        if (RND()<MutationProb) // mutacja pojedynczego genu
          Population[i][j] = 1-Population[i][j];
} // Mutation

void Genetic::CrossingOver(void)
// operacja crossing-over
{
    for (int i=0; i<PopulationSize; i++)
      if (RND()<COProb) {
        int j = random(PopulationSize);
        if (i!=j) {
          int pos = random(ChromosomeSize);
          for (int k=0; k<pos; k++) {
            // zamiana miejscami genw
            char temp = Population[i][k];
            Population[i][k] = Population[j][k];
            Population[j][k] = temp;
          }
        }
      }
} // CrossingOver

float Genetic::GetBinValue(char* Chromosome,
   int Position, int Size)
// zamienia wskazany fragment chromosomu na warto
// rzeczywist z przedziau (0,1)
{
  float val=0;
    for (int i=Position; i<Position+Size; i++)
      val += Chromosome[i]*pow(2.0,-(i-Position+1));
   return val;
} // GetBinValue

void Genetic::Selection(void)
// operacja selekcji
// metoda "ruletki" ze wspczynnikiem zmieniajcym
// bezwzgldno selekcji
// dla SelectionFactor=0 wszystkie osobniki maj tak
// sam szans wejcia - bez wzgldu na przystosowanie
{
  float MinFitness=MAXFLOAT;
  float MaxFitness=-MAXFLOAT;
  int BestChromo, N=0;

    // kopiujemy osobniki z "grupy elitarnej"
    for (int i=0; i<EliteSize; i++)
      memcpy(Population[i], Elite[i], ChromosomeSize);

    // szukamy przedziau zmiennoci funkcji przystosowania
    // oraz najlepszego chromosomu w populacji
    for (i=0; i<PopulationSize; i++) {
      FitnessTable[i] = GetFitness(Population[i],
                                   ChromosomeSize);
      if (FitnessTable[i]<MinFitness)
        MinFitness=FitnessTable[i];
      if (FitnessTable[i]>MaxFitness) {
        MaxFitness=FitnessTable[i];
        BestChromo=i;
      }
    }

    // wywietlamy zawarto najlepszego chromosomu
    ShowChromosome(Population[BestChromo],ChromosomeSize);

    // waciwa selekcja
    // wybr osobnikw do nowej populacji
    if (MaxFitness!=MinFitness) {
      while (N<PopulationSize) {
        int Num=random(PopulationSize);
        if (SelectionFactor*RND()<
          (FitnessTable[Num]-MinFitness)/
          (MaxFitness-MinFitness))
          memcpy(NewPopulation[N++], Population[Num],
          ChromosomeSize);
      }

      // sortujemy osobniki malejco wedug przystosowania
      for (int j=0; j<PopulationSize-1; j++)
        for (i=j; i<PopulationSize; i++) // bubblesort
        if (FitnessTable[i]>FitnessTable[j]) {
          float tmp = FitnessTable[i];
          FitnessTable[i] = FitnessTable[j];
          FitnessTable[j] = tmp;
          char *tmp_chromo = Population[i];
          Population[i] = Population[j];
          Population[j] = tmp_chromo;
        }

      // wybieramy najlepsze do "grupy elitarnej"
      for (int i=0; i<EliteSize; i++)
        memcpy(Elite[i], Population[i], ChromosomeSize);

      // tworzymy now grup osobnikw
      for (i=0; i<PopulationSize; i++)
        memcpy(Population[i], NewPopulation[i],
         ChromosomeSize);
    }
} // Selection

void Genetic::RandomizeChromosome(char *Chromosome)
// utworzenie losowego chromosomu
{
    for (int i=0; i<ChromosomeSize; i++)
      Chromosome[i]=random(2); // 0 lub 1
} // RandomizeChromosome

void Genetic::Epoch(void)
// jeden krok symulacji
{
  Mutation();
  CrossingOver();
  Selection();
} // Epoch

<KONIEC WYDRUKU 2>



<WYDRUK 3 - PLIK HODOWLA.CPP>
// HODOWLA.CPP
// proste zastosowanie algorytmu genetycznego
// "hodowla komputerw"

#include <conio.h>
#include <fstream.h>
#include <iostream.h>

#include "genetic.h"

// maksymalna liczba urzdze w zestawie
#define MaxDevices 7

// maksymalna liczba rodzajw urzdze
// pojedynczego rodzaju
#define MaxTypes 8

// na tylu bitach kodujemy typ urzdzenia
#define NBits 3

// maksymalna cena - warunek brzegowy
#define MaxPrice 5000

typedef struct {
  char Name[80]; // nazwa urzdzenia
  int Price; // cena urzdzenia
  int Value; // subiektywna warto urzdzenia
} Device[MaxTypes]; // element zestawu komputerowego

typedef struct {
  int NumDevices; // liczba urzdze
  Device Devices; // urzdzenia
} Computer[MaxDevices];

class Farm: public Genetic { // wyhoduj sobie komputer...

  int Config[MaxDevices]; // tu przechowujemy konfiguracj
  int Price, Value, MaxDev;
  Computer Comp; // oferta - wykaz podzespow

public:
  Farm(int x, int y, int z) : Genetic(x,y,z) {};
  virtual float GetFitness(char* Chromosome, int Size);
  virtual void ShowChromosome(char* Chromosome, int Size);
  void BuildConfiguration(char *Chromosome, int Size);
  void ReadConfiguration(char *Name);
};


void Farm::BuildConfiguration(char *Chromosome, int Size)
// tworzy konfiguracj zestawu na podstawie chromosomu
{
  Price = 0;
  Value = 0;
  MaxDev = Size/NBits;

    // budujemy konfiguracj
    for (int i=0; i<MaxDev; i++) {
      if (Comp[i].NumDevices>0)
        Config[i] = (int)(MaxTypes*
        GetBinValue(Chromosome,i*NBits,NBits)) %
        Comp[i].NumDevices;
      else
        Config[i] = 0;
      Price += Comp[i].Devices[Config[i]].Price; // cena
      Value += Comp[i].Devices[Config[i]].Value; // warto
    }
}

float Farm::GetFitness(char *Chromosome, int Size)
// funkcja przystosowania
{
    BuildConfiguration(Chromosome, Size);
    if (Price>MaxPrice) Price*=10; // kara

  // zwr stosunek moliwoci do ceny
  return (float)Value/Price;
}

void Farm::ShowChromosome(char *Chromosome, int Size)
// wywietla konfiguracj
{
    BuildConfiguration(Chromosome, Size);

    // wywietlamy konfiguracj
    cout << "Najlepsza konfiguracja:\n";
    cout << "Cena: " << Price;
    cout << " z, warto: " << Value << "\n";
    for (int i=0; i<MaxDev; i++)
      cout << Comp[i].Devices[Config[i]].Name << "\n";
    cout << "\n";
}

void Farm::ReadConfiguration(char *Name)
// wczytuje plik z podzespoami
{
    ifstream in_file(Name, ios::in);
    // wczytujemy ofert - spis dostpnych podzespow

    for (int i=0; i<MaxDevices; i++) {
      in_file >> Comp[i].NumDevices;
      in_file.ignore(80,'\n'); // ignoruj do nastepnej linii
      if (Comp[i].NumDevices>MaxTypes)
        Comp[i].NumDevices=MaxTypes;
      if (Comp[i].NumDevices)
      for (int j=0; j<Comp[i].NumDevices; j++) {
        in_file.getline(Comp[i].Devices[j].Name, 80, '\n');
        in_file >> Comp[i].Devices[j].Price;
        in_file.ignore(80,'\n');
        in_file >> Comp[i].Devices[j].Value;
        in_file.ignore(80,'\n');
      }
    }

    in_file.close(); // ok - oferta wczytana
}

void main()
{
  Farm* Gen;

    // nowa kodowla - 20 osobnikw
    // grupa elitarna - 5 osobnikw
    // rozmiar chromosomu - MaxDevices*NBits
    Gen = new Farm(20,5,MaxDevices*NBits);

    // wczytanie spisu podzespow
    Gen->ReadConfiguration("OFERTA.TXT");

    // symulacja ... a do nacinicia
    // dowolnego klawisza
    while (!kbhit()) Gen->Epoch();

    // koniec
    delete Gen;
}

<KONIEC WYDRUKU 3>


<WYDRUK 4 - PLIK OFERTA.TXT>

4 // liczba monitorw
Monitor mono 14"
300 // cena monitora
100 // subiektywna warto monitora
Monitor kolor 14"
600
450
Monitor kolor 14" NI
620
500
Monitor kolor 15" NI
1100
600
7 // procesory
486 DX-33
120
120
486 DX2-66
160
200
486 DX2-80
200
250
486 DX4-100
220
300
486 DX4-120
280
350
Pentium 75
550
500
Pentium 100
750
700
3 // pami
4 MB RAM
360
300
8 MB RAM
650
600
16 MB RAM
1100
850
2 // napdy dyskietek
FDD 1.44"
90
100
FDD 1.44" + FDD 1.2"
190
150
3 // dysk
HDD 520 MB
450
350
HDD 850 MB
550
450
HDD 1200 MB
700
550
3 // CD-ROM
Bez CDROM-u
0
0
CDROM 2x
300
300
CDROM 4x
450
350
1 // reszta elementw (obudowa+zasilacz+pyta etc.)
Obudowa + zasilacz + pyta gowna + karty + ...
800
800

<KONIEC WYDRUKU 4>
